winbrew_app\operations\install/
download.rs1use anyhow::Result;
13use std::path::Path;
14
15use crate::core::cancel::check;
16use crate::core::fs::{cleanup_path, finalize_temp_file};
17use crate::core::hash::{Hasher, verify_hash};
18use crate::core::network::{build_client as network_build_client, download_url_to_temp_file};
19use crate::models::catalog::CatalogInstaller;
20use crate::models::domains::shared::HashAlgorithm;
21
22const CATALOG_USER_AGENT: &str = "winbrew-package-installer";
23
24pub fn build_client() -> Result<crate::core::network::Client> {
29 Ok(network_build_client(CATALOG_USER_AGENT)?)
30}
31
32pub fn download_installer<FStart, FProgress>(
43 client: &crate::core::network::Client,
44 installer: &CatalogInstaller,
45 download_path: &Path,
46 ignore_checksum_security: bool,
47 on_start: FStart,
48 mut on_progress: FProgress,
49) -> Result<Vec<HashAlgorithm>>
50where
51 FStart: FnOnce(Option<u64>),
52 FProgress: FnMut(u64),
53{
54 let temp_path = download_path.with_extension("part");
55 let download_result = (|| -> Result<Vec<HashAlgorithm>> {
56 check()?;
57
58 let (verification, legacy_checksum_algorithms) = verify_strategy(
59 &installer.hash,
60 installer.hash_algorithm,
61 ignore_checksum_security,
62 )?;
63 let mut verification = verification;
64
65 check()?;
66
67 download_url_to_temp_file(
68 client,
69 &installer.url,
70 &temp_path,
71 "installer",
72 on_start,
73 &mut on_progress,
74 |chunk| {
75 check()?;
76 verification.update(chunk);
77 Ok(())
78 },
79 )?;
80
81 verification.finish(&installer.hash)?;
82
83 finalize_temp_file(&temp_path, download_path)?;
84
85 Ok(legacy_checksum_algorithms)
86 })();
87
88 if download_result.is_err() {
89 let _ = cleanup_path(&temp_path);
90 }
91
92 download_result
93}
94
95enum Verification {
96 None,
97 Active(Box<Hasher>),
98}
99
100impl Verification {
101 fn update(&mut self, chunk: &[u8]) {
102 match self {
103 Self::None => {}
104 Self::Active(hasher) => hasher.update(chunk),
105 }
106 }
107
108 fn finish(self, expected_hash: &str) -> Result<()> {
109 match self {
110 Self::None => Ok(()),
111 Self::Active(hasher) => {
112 verify_hash(expected_hash, hasher.finalize()).map_err(Into::into)
113 }
114 }
115 }
116}
117
118fn verify_strategy(
119 expected_hash: &str,
120 hash_algorithm: HashAlgorithm,
121 ignore_checksum_security: bool,
122) -> Result<(Verification, Vec<HashAlgorithm>)> {
123 let trimmed = expected_hash.trim();
124
125 if trimmed.is_empty() {
126 return Ok((Verification::None, Vec::new()));
127 }
128
129 match hash_algorithm {
130 HashAlgorithm::Md5 if ignore_checksum_security => {
133 Ok((Verification::None, vec![HashAlgorithm::Md5]))
134 }
135 HashAlgorithm::Md5 => Err(crate::core::HashError::LegacyChecksumAlgorithm {
136 algorithm: HashAlgorithm::Md5,
137 }
138 .into()),
139 HashAlgorithm::Sha1 if ignore_checksum_security => Ok((
140 Verification::Active(Box::new(Hasher::new(HashAlgorithm::Sha1))),
141 vec![HashAlgorithm::Sha1],
142 )),
143 HashAlgorithm::Sha1 => Err(crate::core::HashError::LegacyChecksumAlgorithm {
144 algorithm: HashAlgorithm::Sha1,
145 }
146 .into()),
147 algorithm => Ok((
148 Verification::Active(Box::new(Hasher::new(algorithm))),
149 Vec::new(),
150 )),
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::{Verification, verify_strategy};
157 use crate::core::HashError;
158 use crate::models::domains::shared::HashAlgorithm;
159
160 #[test]
161 fn verify_strategy_rejects_md5_without_ignore_flag() {
162 let err = match verify_strategy("abc123", HashAlgorithm::Md5, false) {
163 Ok(_) => panic!("md5 should be rejected by default"),
164 Err(err) => err,
165 };
166
167 assert!(matches!(
168 err.downcast_ref::<HashError>(),
169 Some(HashError::LegacyChecksumAlgorithm {
170 algorithm: HashAlgorithm::Md5
171 })
172 ));
173 }
174
175 #[test]
176 fn verify_strategy_tolerates_md5_with_ignore_flag() {
177 let (verification, legacy_checksum_algorithms) =
178 verify_strategy("abc123", HashAlgorithm::Md5, true)
179 .expect("md5 should be tolerated when ignoring checksum security");
180
181 assert!(matches!(verification, Verification::None));
182 assert_eq!(legacy_checksum_algorithms, vec![HashAlgorithm::Md5]);
183 }
184
185 #[test]
186 fn verify_strategy_skips_verification_for_empty_hash() {
187 let (verification, legacy_checksum_algorithms) =
188 verify_strategy(" ", HashAlgorithm::Sha256, false)
189 .expect("empty hashes should bypass verification");
190
191 assert!(matches!(verification, Verification::None));
192 assert!(legacy_checksum_algorithms.is_empty());
193 }
194
195 #[test]
196 fn verify_strategy_still_verifies_sha1_when_ignored() {
197 let (verification, legacy_checksum_algorithms) =
198 verify_strategy("abc123", HashAlgorithm::Sha1, true)
199 .expect("sha1 should remain verifiable when security checks are ignored");
200
201 assert!(matches!(verification, Verification::Active(_)));
202 assert_eq!(legacy_checksum_algorithms, vec![HashAlgorithm::Sha1]);
203 }
204}